package advance;

import java.awt.Component;
import java.awt.Rectangle;
import java.awt.geom.Area;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import map.AbstractMap;
import mapelements.background.Background;
import mapelements.foreground.Foreground;

import errors.AlreadyUsedKeyException;
import errors.CoordinatesNotAcceptableException;
import errors.DimensionNotAcceptableException;
import errors.KeyNotFoundException;
import errors.StratumNotFoundException;


/**rappresenta una mappa in cui i foreground vengono allocati all'interno
 * di una serie di HashMap. Questo modo di allocare i dati provoca le seguenti conseguenze:
 * <ul>
 *  <li>i tempi di accesso sono piu' lenti</li>
 *  <li>l'identificatore che caratterizza un Foreground e' complesso (una stringa)</li>
 *  <li>in caso in cui un foreground debba comunicare con un altro e' possibile richiamare
 *  il secondo se si costruire in maniera consona gli identificatori di stringa</li>
 *  <li>e' possibile, tramite una corretta identificazione, risalire in maniera automatica
 *  ad un foreground, invece di ricercare in una lista lo stesso</li>
 * </ul> 
 * 
 * Una forte ipotesi è che la LevelBasedMap <strong>non deve contenere foreground di valore NULL</strong>.
 * 
 * <p>Per quanto riguarda le scelte che la AbstractMap pone, la LevelBasedMap sceglie queste casitiche:
 * <ul>
 *  <li>{@link #getFloorForeground(Object)} ritorna il primo foreground che il coputer trova, senza guardare la porzione più
 *  "nascosta" dei foregrounds</li>
 *  <li>le coordinate (x;y) dei MapObject indicano il punto in alto a sinistra dei MapObject</li>
 *  <li>la shape indica i confini (in termini di mapResolution) del MapObject con riferimento il punto (x;y)</li>
 * </ul>
 * 
 * @author Koldar
 * @version 1.5
 */
public class LevelBasedMap extends AbstractMap{

	private static final long serialVersionUID = -2243567289212321910L;
	/**rappresenta una lista di mappe che associa ad una stringa un particolare Foreground.*/
	protected List<HashMap<String,Foreground>> foregroundMap;
	
	protected LevelBasedMap(int _mapresolution, int _width, int _height)throws DimensionNotAcceptableException {
		this(_mapresolution, _width, _height,null);
	}
	/**costruisce una nuova mappa con un certo background ma sorpattutto con certe dimensioni.Se in costruzione
	 * viene lanciato DimensionNotAcceptableException la mappa è da considerarsi corrotta e va dunque rimpiazzata  
	 * 
	 * @param _mapresolution
	 * @param _width
	 * @param _height
	 * @param background
	 * @throws DimensionNotAcceptableException
	 */
	protected LevelBasedMap(int _mapresolution, int _width, int _height,Background background)throws DimensionNotAcceptableException {
		super(_mapresolution, _width, _height, background);
		this.foregroundMap=new Vector<HashMap<String,Foreground>>(0,1);
	}
	
	/**costruisce una nuova mappa con già il primo strato di foreground impostato. tale strato è preso dal parametro <tt>map</tt>
	 * 
	 * @param _mapresolution
	 * @param _width
	 * @param _height
	 * @param background
	 * @param map
	 * @throws DimensionNotAcceptableException
	 */
	protected LevelBasedMap(int _mapresolution, int _width, int _height,Background background,HashMap map)throws DimensionNotAcceptableException {
		super(_mapresolution, _width, _height, background);
		this.foregroundMap=new Vector<HashMap<String,Foreground>>(0,1);	
	}
	
	@Override
	public void addFloorStratum() {
		this.addStratum(0);
	}
	@Override
	public void addForeground(int stratum, Object key,Foreground foreground) throws StratumNotFoundException,AlreadyUsedKeyException,CoordinatesNotAcceptableException {
		if (this.getKeys().contains(key)){
			throw new AlreadyUsedKeyException(key);
		}
		try{
			this.isShapeContained(foreground);
			this.foregroundMap.get(stratum).put(key.toString(), foreground);
			foreground.start();
		} catch (IndexOutOfBoundsException error){
			throw new StratumNotFoundException(stratum);
		}
		this.fireInsertForegroundEvent(foreground);
	}
	
	@Override
	public void addStratum() {
		this.foregroundMap.add(new HashMap<String,Foreground>());
	}
	
	@Override
	public void addStratum(int bottomstratum) throws StratumNotFoundException {
		try{
			this.foregroundMap.add(bottomstratum, new HashMap<String,Foreground>());
		} catch (IndexOutOfBoundsException error){
			throw new StratumNotFoundException(bottomstratum);
		}
	}
	
	@Override
	public void addUnsecureForeground(int stratum, Object key,Foreground foreground) throws StratumNotFoundException,CoordinatesNotAcceptableException {
		try{
			this.isShapeContained(foreground);
			this.foregroundMap.get(stratum).put(key.toString(), foreground);
			foreground.start();
		} catch (IndexOutOfBoundsException error){
			throw new StratumNotFoundException(stratum);
		}
		this.fireInsertForegroundEvent(foreground);
	}
	@Override
	public LevelBasedMap clone() throws CloneNotSupportedException{
		LevelBasedMap result=(LevelBasedMap)super.clone();
		result.foregroundMap=new Vector<HashMap<String,Foreground>>(this.foregroundMap);
		return result;
	}
	/**data la chiave che identifica il foreground ritorna il primo foreground
	 * che il computer individua che ha un'area non vuota ottenuta intersecando la <tt>shape</tt>
	 * del primo foreground con la <tt>shape</tt> del secondo
	 * 
	 */
	@Override
	public Foreground getFloorForeground(Object key) throws KeyNotFoundException{
		int stratum=this.getForegroundStratum(key);
		Foreground foreground=this.getForeground(key);
		Area foregroundArea=new Area(foreground.getShape());
		Area testArea=null;
		try{
			for (int i=stratum-1;i>=0;i--){
				for (Foreground f:this.getStratum(stratum).values()){
					testArea=new Area(f.getShape());
					testArea.intersect(foregroundArea);
					if (!testArea.isEmpty()){
						return f;
					}
				}
			}
		}catch (StratumNotFoundException error){
		}
		return null;
	}
	
	@Override
	public List<Foreground> getFloorStackForeground(Object key) throws KeyNotFoundException{
		
		// TODO non ne ho la minima idea di come fare :P
		return null;
	}
	@Override
	public Foreground getForeground(Object key) throws KeyNotFoundException {
		for (int i=0;i<this.foregroundMap.size();i++){
			if (this.foregroundMap.get(i).containsKey(key)){
				return this.foregroundMap.get(i).get(key);
			}
		}
		throw new KeyNotFoundException(key.toString());
	}
	@Override
	public int getForegroundStratum(Object key) throws KeyNotFoundException {
		for (int i=0;i<this.foregroundMap.size();i++){
			if (this.foregroundMap.get(i).containsKey(key)){
				return i;
			}
		}
		throw new KeyNotFoundException(key);
	}
	
	@Override
	@Deprecated
	public String getKey(Foreground frg) throws KeyNotFoundException {
		for (HashMap<String,Foreground> m:this.foregroundMap){
			//TODO da fare... speriamo che non serva  ^^
		}
		throw new KeyNotFoundException("???");
	}
	@Override
	public Set<String> getKeys() {
		Set<String> result=new HashSet<String>();
		for (HashMap<String,Foreground> map:this.foregroundMap){
			result.addAll(map.keySet());
		}
		return result;
	}
	
	@Override
	public int getStratiSize() {
		return this.foregroundMap.size();
	}
	@Override
	public HashMap<String,Foreground> getStratum(int index) throws StratumNotFoundException {
		try{
			return this.foregroundMap.get(index);
		} catch (IndexOutOfBoundsException error){
			throw new StratumNotFoundException(index);
		}
	}
	
	@Override
	public void removeForeground(Object key) throws KeyNotFoundException {
		Foreground removed;
		for (int i=0;i<this.foregroundMap.size();i++){
			removed=this.foregroundMap.get(i).remove(key);
			if (removed!=null){
				this.fireDeleteForegroundEvent(removed);
				return;
			}
		}
		throw new KeyNotFoundException(key.toString());
	}
	
	@Override
	public void removeStratum(int idstratum) throws StratumNotFoundException {
		try{
			this.foregroundMap.remove(idstratum);
		} catch (IndexOutOfBoundsException error){
			throw new StratumNotFoundException(idstratum);
		}
	}
	@Override
	public void setDrawingComponent(Component parent){
		super.setDrawingComponent(parent);
		for (HashMap<String,Foreground> m: this.foregroundMap){
			for (Foreground f:m.values()){
				try{
					f.getRenderer().setDrawingComponent(parent);
				} catch(NullPointerException error){}
			}
		}
	}
	/**oltre ad impostare le dimensioni e il background della mappa, questo metodo controlla che la mappa passata come
	 * parametro sia di tipo <tt>LevelBasedMap</tt>: se lo è aggiorna gli statri di questa mappa in modo che coincidano con quella della mappa
	 * passata come parametro. Inoltre esegue i metodi <code>terminate()</code> dei vecchi foreground della mappa e <code>start()</code> dei
	 * nuovi foreground impostati nella mappa.
	 */
	@Override
	public void setMap(AbstractMap othermap) {
		super.setMap(othermap);
		//terminazione dei precedenti foreground su questa mappa
		for (String key:this.getKeys()){
			this.getForeground(key).terminate();
		}
		this.foregroundMap=(othermap instanceof LevelBasedMap?((LevelBasedMap)othermap).foregroundMap:new Vector<HashMap<String,Foreground>>(0,1));
		//avvio dei nuovi foreground
		for (String key: this.getKeys()){
			this.getForeground(key).start();
		}
		//FIXME qui ci dovrebbe essere anche una trasmissione di un evento upgradeForeground!
	}
	
	@Override
	public void terminate(){
		super.terminate();
	}
	@Override
	public void upgradeForeground(Object key, Foreground newforeground)throws KeyNotFoundException,CoordinatesNotAcceptableException {
		this.isShapeContained(newforeground);
		for (HashMap<String,Foreground> map:this.foregroundMap){
			if (map.containsKey(key)){
				Foreground old=map.get(key.toString());
				map.put(key.toString(),newforeground);
				this.fireUpgradeForegroundEvent(newforeground);
				return;
			}
		}
		throw new KeyNotFoundException(key);
	}
	
	/**verifica che il foreground passato sia completamente inserito all'interno della mappa e non possieda alcun elemento al di fuori di essa
	 * 
	 * @param toverify
	 * @throws CoordinatesNotAcceptableException in caso il foreground possieda qualche suo elemento fuori dalla mappa
	 */
	private void isShapeContained(Foreground toverify) throws CoordinatesNotAcceptableException{
		try{
			if (!(new Rectangle(0,0,this.width,this.height)).contains(toverify.getAbsoluteShape().getBounds())){
				throw new CoordinatesNotAcceptableException(toverify.getAbsoluteShape(),new Rectangle(0,0,this.width,this.height));
			}
		}catch (UnsupportedOperationException error){
			//per default il metodo da per ipotesi che la forma non è valida
			throw new CoordinatesNotAcceptableException(toverify.getAbsoluteShape(),new Rectangle(0,0,this.width,this.height));
		}
	}
	
	
	

}
